Skip to content

feat(completion): smart paren detection to avoid duplicate parens#415

Open
16bit-ykiko wants to merge 1 commit intomainfrom
feat/completion-smart-parens
Open

feat(completion): smart paren detection to avoid duplicate parens#415
16bit-ykiko wants to merge 1 commit intomainfrom
feat/completion-smart-parens

Conversation

@16bit-ykiko
Copy link
Copy Markdown
Member

@16bit-ykiko 16bit-ykiko commented Apr 9, 2026

Summary

  • When the next non-whitespace character after the cursor is already (, skip inserting parentheses and parameter placeholders in the snippet
  • Prevents duplicate parens when completing a function name that already has arguments: fo|(args) → inserts just foooo, not foooo(${1:...})
  • Uses the original file content for lookahead (not Clang's internal buffer which may be modified during completion)

Test plan

  • SmartParensSkip — next char is (, no parens inserted
  • SmartParensInsert — next char is ;, parens inserted normally
  • All 496 unit tests pass
  • pixi run format clean

Stacked on #412.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced code completion with smart parentheses handling—completion now intelligently detects when a function name is already followed by a parenthesis in your code and avoids inserting a duplicate. Argument snippets are properly inserted when no parenthesis is present.
  • Tests

    • Added unit tests to verify smart parentheses behavior in code completion.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

The PR enhances code completion snippet generation by implementing smart parentheses detection. When the completion cursor is immediately followed by a ( token in the source text, the generated snippet omits parentheses and argument placeholders to prevent duplicate syntax. This is achieved via lookahead inspection of the original buffer content.

Changes

Cohort / File(s) Summary
Smart Parentheses Detection
src/feature/code_completion.cpp
Added next_token_char() helper for whitespace-skipping lookahead. Extended build_snippet() with skip_parens flag to conditionally omit parentheses and placeholder chunks. Updated CodeCompletionCollector constructor to receive and store original_content parameter. Modified snippet generation to detect if next token is ( and pass result to build_snippet().
Unit Tests
tests/unit/feature/code_completion_tests.cpp
Added two new tests: SmartParensSkip verifies parentheses omission when ( immediately follows cursor; SmartParensInsert verifies normal snippet generation when no ( follows.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A rabbit hops through brackets bright,
Checking parens left and right,
"No double ( for this completion!" ✨
Smart snippets with intention,
Arguments dance—just what's right! 🎯

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing smart parenthesis detection to prevent duplicate parentheses in code completion.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/completion-smart-parens

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Base automatically changed from feat/completion-snippets to main April 9, 2026 08:39
When the next non-whitespace character after the cursor is already '(',
skip inserting parentheses and parameter placeholders in the snippet.
This prevents duplicate parens when completing a function name that
already has arguments written after it.

Uses the original file content (not Clang's internal buffer) for
lookahead to correctly detect the next token.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@16bit-ykiko 16bit-ykiko force-pushed the feat/completion-smart-parens branch from 680086a to 8e9d4fa Compare April 18, 2026 06:57
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/feature/code_completion.cpp (1)

232-236: Misleading comment.

The guard is unconditional on placeholder_index == 0 — it fires whether or not skip_parens was set (e.g. void foo() with skip_parens=false also returns {}). The comment implies a combined condition. Either tighten the comment or make the check match it.

📝 Suggested comment tweak
-    // If no placeholders were generated and parens were skipped,
-    // return empty to signal plain text.
+    // If no placeholders were generated, signal plain text by returning empty
+    // (callers fall back to the label). This covers no-arg functions and the
+    // skip_parens=true case where all placeholders were suppressed.
     if(placeholder_index == 0) {
         return {};
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/feature/code_completion.cpp` around lines 232 - 236, The comment
incorrectly states the guard depends on skip_parens but the code only checks
placeholder_index; update the conditional to match the comment by changing the
guard to check both placeholder_index == 0 and skip_parens (e.g.,
if(placeholder_index == 0 && skip_parens) { return {}; }) or alternatively
change the comment to say the return is unconditional when no placeholders were
generated; locate the check around placeholder_index and skip_parens in the
function in src/feature/code_completion.cpp and apply one of these two fixes to
keep code and comment consistent.
tests/unit/feature/code_completion_tests.cpp (1)

338-374: Consider adding a whitespace-between-cursor-and-paren test.

Both new tests are well-targeted, but neither exercises the whitespace-skipping branch of next_token_char (the whole reason we iterate instead of just peeking content[offset]). An extra case like int z = fo$(pos) (1, 2.0f); (with spaces/newlines before the () would lock in that behavior and prevent accidental regressions if someone later simplifies the lookahead to a single-char check.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/feature/code_completion_tests.cpp` around lines 338 - 374, Add a
new unit test that mirrors SmartParensSkip but places whitespace/newlines
between the cursor and the following '(' to exercise the whitespace-skipping
branch used by next_token_char; call it e.g. SmartParensSkipWithWhitespace, set
feature::CodeCompletionOptions the same way as the other tests
(bundle_overloads=false, enable_function_arguments_snippet=true), invoke
code_complete with a snippet like "int foooo(...); int z = fo$(pos)   (1,
2.0f);" and then locate the completion via find_item("foooo") and assert (like
SmartParensSkip) that the resolved protocol::TextEdit new_text does not contain
"(" so the snippet degrades to plain text.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/feature/code_completion.cpp`:
- Around line 161-171: next_token_char currently only skips whitespace and will
return '/' when a comment follows the cursor, breaking smart-paren logic; update
next_token_char to also skip C++-style comments by detecting '/' then peeking
the next char: if "//" advance i until newline or end, if "/*" advance i until
the matching "*/" or end (being careful with bounds), otherwise treat '/' as a
token; continue the outer search after skipping comments and return '\0' on EOF.
Ensure you reference and update the loop and index handling in next_token_char
to correctly advance past multi-char comment sequences and avoid out-of-bounds
peeks.
- Around line 262-266: The constructor's member initializer list initializes
members in the wrong order causing -Wreorder: reorder the initializers to match
the declaration order (offset, encoding, output, original_content, options,
info) — specifically swap the positions of options and original_content in the
initializer list so original_content is initialized before options (keep info
last as std::make_shared<clang::GlobalCodeCompletionAllocator>()).

---

Nitpick comments:
In `@src/feature/code_completion.cpp`:
- Around line 232-236: The comment incorrectly states the guard depends on
skip_parens but the code only checks placeholder_index; update the conditional
to match the comment by changing the guard to check both placeholder_index == 0
and skip_parens (e.g., if(placeholder_index == 0 && skip_parens) { return {}; })
or alternatively change the comment to say the return is unconditional when no
placeholders were generated; locate the check around placeholder_index and
skip_parens in the function in src/feature/code_completion.cpp and apply one of
these two fixes to keep code and comment consistent.

In `@tests/unit/feature/code_completion_tests.cpp`:
- Around line 338-374: Add a new unit test that mirrors SmartParensSkip but
places whitespace/newlines between the cursor and the following '(' to exercise
the whitespace-skipping branch used by next_token_char; call it e.g.
SmartParensSkipWithWhitespace, set feature::CodeCompletionOptions the same way
as the other tests (bundle_overloads=false,
enable_function_arguments_snippet=true), invoke code_complete with a snippet
like "int foooo(...); int z = fo$(pos)   (1, 2.0f);" and then locate the
completion via find_item("foooo") and assert (like SmartParensSkip) that the
resolved protocol::TextEdit new_text does not contain "(" so the snippet
degrades to plain text.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f457ba2a-a0b1-4533-9555-12f585fab066

📥 Commits

Reviewing files that changed from the base of the PR and between 418e190 and 8e9d4fa.

📒 Files selected for processing (2)
  • src/feature/code_completion.cpp
  • tests/unit/feature/code_completion_tests.cpp

Comment on lines +161 to +171
/// Find the first non-whitespace character after the given offset in content.
/// Returns '\0' if none found (end of content).
auto next_token_char(llvm::StringRef content, std::uint32_t offset) -> char {
for(auto i = offset; i < content.size(); ++i) {
char c = content[i];
if(c != ' ' && c != '\t' && c != '\n' && c != '\r') {
return c;
}
}
return '\0';
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Lookahead does not skip comments.

next_token_char only skips whitespace. A cursor followed by a comment before the (, such as foo|/* comment */(args) or foo| // comment\n(args), will return / and fail to trigger smart-paren skipping, causing duplicate parens in those (uncommon but real) cases. If you want this to be robust, consider skipping // line comments and /* */ block comments too. Otherwise this is an acceptable limitation worth a doc note.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/feature/code_completion.cpp` around lines 161 - 171, next_token_char
currently only skips whitespace and will return '/' when a comment follows the
cursor, breaking smart-paren logic; update next_token_char to also skip
C++-style comments by detecting '/' then peeking the next char: if "//" advance
i until newline or end, if "/*" advance i until the matching "*/" or end (being
careful with bounds), otherwise treat '/' as a token; continue the outer search
after skipping comments and return '\0' on EOF. Ensure you reference and update
the loop and index handling in next_token_char to correctly advance past
multi-char comment sequences and avoid out-of-bounds peeks.

Comment on lines +262 to +266
const CodeCompletionOptions& options,
llvm::StringRef original_content) :
clang::CodeCompleteConsumer({}), offset(offset), encoding(encoding), output(output),
options(options), info(std::make_shared<clang::GlobalCodeCompletionAllocator>()) {}
options(options), original_content(original_content),
info(std::make_shared<clang::GlobalCodeCompletionAllocator>()) {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the member declaration order vs initializer order in the file under review.
rg -nP -C1 '^\s*(std::uint32_t offset;|PositionEncoding encoding;|std::vector<protocol::CompletionItem>& output;|llvm::StringRef original_content;|const CodeCompletionOptions& options;|clang::CodeCompletionTUInfo info;)' src/feature/code_completion.cpp
rg -nP -C2 'clang::CodeCompleteConsumer\(\{\}\), offset\(offset\)' src/feature/code_completion.cpp

Repository: clice-io/clice

Length of output: 881


Member initializer order does not match declaration order (-Wreorder).

The data members are declared as offset (528), encoding (529), output (530), original_content (531), options (532), but initialized as offset, encoding, output, options, original_content (lines 264-265). C++ requires member initialization in declaration order; this mismatch triggers -Wreorder, which will fail with -Werror.

🔧 Proposed fix
 private:
     std::uint32_t offset;
     PositionEncoding encoding;
     std::vector<protocol::CompletionItem>& output;
-    llvm::StringRef original_content;
     const CodeCompletionOptions& options;
+    llvm::StringRef original_content;
     clang::CodeCompletionTUInfo info;

Or swap the initializers on lines 265-266 to match declaration order.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/feature/code_completion.cpp` around lines 262 - 266, The constructor's
member initializer list initializes members in the wrong order causing
-Wreorder: reorder the initializers to match the declaration order (offset,
encoding, output, original_content, options, info) — specifically swap the
positions of options and original_content in the initializer list so
original_content is initialized before options (keep info last as
std::make_shared<clang::GlobalCodeCompletionAllocator>()).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant